import { expect } from 'chai';
import { spy } from 'sinon';
import { act, flushMicrotasks, fireEvent, screen, waitFor } from '@mui/internal-test-utils';
import {
  DirectionProvider,
  type TextDirection,
} from '@base-ui-components/react/direction-provider';
import { Popover } from '@base-ui-components/react/popover';
import { Dialog } from '@base-ui-components/react/dialog';
import { Tabs } from '@base-ui-components/react/tabs';
import { createRenderer, describeConformance, isJSDOM } from '#test-utils';

describe('<Tabs.Root />', () => {
  const { render } = createRenderer();

  beforeEach(function beforeHook({ skip }) {
    const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);

    // The test fails on Safari with just:
    //
    // container.scrollLeft = 200;
    // expect(container.scrollLeft).to.equal(200); 💥
    if (isSafari) {
      skip();
    }
  });

  describeConformance(<Tabs.Root value={0} />, () => ({
    render,
    refInstanceof: window.HTMLDivElement,
  }));

  describe('prop: children', () => {
    it('should accept a null child', async () => {
      await render(
        <Tabs.Root value={0}>
          {null}
          <Tabs.List>
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      expect(screen.getAllByRole('tab')).to.have.lengthOf(1);
    });

    it('should support empty children', async () => {
      await render(<Tabs.Root value={1} />);
    });

    it('puts the selected child in tab order', async () => {
      const { setProps } = await render(
        <Tabs.Root value={1}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      expect(screen.getAllByRole('tab').map((tab) => tab.tabIndex)).to.have.ordered.members([
        -1, 0,
      ]);

      await setProps({ value: 0 });

      expect(screen.getAllByRole('tab').map((tab) => tab.tabIndex)).to.have.ordered.members([
        0, -1,
      ]);
    });

    it('sets the aria-labelledby attribute on tab panels to the corresponding tab id', async () => {
      await render(
        <Tabs.Root defaultValue="tab-0">
          <Tabs.List>
            <Tabs.Tab value="tab-0" />
            <Tabs.Tab value="tab-1" id="explicit-tab-id-1" />
            <Tabs.Tab value="tab-2" />
            <Tabs.Tab value="tab-3" id="explicit-tab-id-3" />
          </Tabs.List>
          <Tabs.Panel value="tab-1" keepMounted />
          <Tabs.Panel value="tab-0" keepMounted />
          <Tabs.Panel value="tab-2" keepMounted />
          <Tabs.Panel value="tab-3" keepMounted />
        </Tabs.Root>,
      );

      const tabs = screen.getAllByRole('tab');
      const tabPanels = screen.getAllByRole('tabpanel', { hidden: true });

      expect(tabPanels[0]).to.have.attribute('aria-labelledby', tabs[1].id);
      expect(tabPanels[1]).to.have.attribute('aria-labelledby', tabs[0].id);
      expect(tabPanels[2]).to.have.attribute('aria-labelledby', tabs[2].id);
      expect(tabPanels[3]).to.have.attribute('aria-labelledby', tabs[3].id);
    });

    it('sets the aria-controls attribute on tabs to the corresponding tab panel id', async () => {
      await render(
        <Tabs.Root defaultValue="tab-0">
          <Tabs.List>
            <Tabs.Tab value="tab-0" />
            <Tabs.Tab value="tab-1" id="explicit-tab-id-1" />
            <Tabs.Tab value="tab-2" />
            <Tabs.Tab value="tab-3" id="explicit-tab-id-3" />
          </Tabs.List>
          <Tabs.Panel value="tab-1" keepMounted />
          <Tabs.Panel value="tab-0" keepMounted />
          <Tabs.Panel value="tab-2" keepMounted />
          <Tabs.Panel value="tab-3" keepMounted />
        </Tabs.Root>,
      );

      const tabs = screen.getAllByRole('tab');
      const tabPanels = screen.getAllByRole('tabpanel', { hidden: true });

      expect(tabs[0]).to.have.attribute('aria-controls', tabPanels[1].id);
      expect(tabs[1]).to.have.attribute('aria-controls', tabPanels[0].id);
      expect(tabs[2]).to.have.attribute('aria-controls', tabPanels[2].id);
      expect(tabs[3]).to.have.attribute('aria-controls', tabPanels[3].id);
    });

    it('sets aria-controls on the first tab when no value is provided', async () => {
      await render(
        <Tabs.Root>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
          <Tabs.Panel value={0} keepMounted />
          <Tabs.Panel value={1} keepMounted />
        </Tabs.Root>,
      );

      const tabs = screen.getAllByRole('tab');
      const tabPanels = screen.getAllByRole('tabpanel', { hidden: true });

      expect(tabs[0]).to.have.attribute('aria-controls', tabPanels[0].id);
      expect(tabs[1]).to.have.attribute('aria-controls', tabPanels[1].id);
      expect(tabPanels[0]).to.have.attribute('aria-labelledby', tabs[0].id);
      expect(tabPanels[1]).to.have.attribute('aria-labelledby', tabs[1].id);
    });

    it('syncs aria-controls to the mounted tab panel when keepMounted is false', async () => {
      const { user } = await render(
        <Tabs.Root defaultValue="tab-0">
          <Tabs.List>
            <Tabs.Tab value="tab-0">Tab 0</Tabs.Tab>
            <Tabs.Tab value="tab-1">Tab 1</Tabs.Tab>
          </Tabs.List>
          <Tabs.Panel value="tab-0">Panel 0</Tabs.Panel>
          <Tabs.Panel value="tab-1">Panel 1</Tabs.Panel>
        </Tabs.Root>,
      );

      const tabs = screen.getAllByRole('tab');
      const [firstTabPanel] = screen.getAllByRole('tabpanel');

      expect(tabs[0]).to.have.attribute('aria-controls', firstTabPanel.id);
      expect(tabs[1]).not.to.have.attribute('aria-controls');

      await user.click(tabs[1]);

      await waitFor(() => {
        const [secondTabPanel] = screen.getAllByRole('tabpanel');

        expect(secondTabPanel).to.have.text('Panel 1');
        expect(tabs[0]).not.to.have.attribute('aria-controls');
        expect(tabs[1]).to.have.attribute('aria-controls', secondTabPanel.id);
      });
    });
  });

  describe('prop: value', () => {
    it('should pass selected prop to children', async () => {
      const tabs = (
        <Tabs.Root value={1}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>
      );

      await render(tabs);
      const tabElements = screen.getAllByRole('tab');
      expect(tabElements[0]).to.have.attribute('aria-selected', 'false');
      expect(tabElements[1]).to.have.attribute('aria-selected', 'true');
    });

    it('should support values of different types', async () => {
      const tabValues = [0, '1', { value: 2 }, () => 3, Symbol('4'), /5/];

      await render(
        <Tabs.Root>
          <Tabs.List>
            {tabValues.map((value, index) => (
              <Tabs.Tab key={index} value={value} />
            ))}
          </Tabs.List>
          {tabValues.map((value, index) => (
            <Tabs.Panel key={index} value={value} keepMounted />
          ))}
        </Tabs.Root>,
      );

      const tabElements = screen.getAllByRole('tab');
      const tabPanelElements = screen.getAllByRole('tabpanel', { hidden: true });

      await Promise.allSettled(
        tabValues.map(async (value, index) => {
          expect(tabPanelElements[index]).to.have.attribute(
            'aria-labelledby',
            tabElements[index].id,
          );

          await act(() => {
            tabElements[index].click();
          });

          expect(tabPanelElements[index]).not.to.have.attribute('hidden');
        }),
      );
    });
  });

  describe('prop: onValueChange', () => {
    it('when `activateOnFocus = true` should call onValueChange on pointerdown', async () => {
      const handleChange = spy();
      const handlePointerDown = spy();
      const { user } = await render(
        <Tabs.Root value={0} onValueChange={handleChange}>
          <Tabs.List activateOnFocus>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} onPointerDown={handlePointerDown} />
          </Tabs.List>
        </Tabs.Root>,
      );

      await user.pointer({ keys: '[MouseLeft>]', target: screen.getAllByRole('tab')[1] });
      expect(handleChange.callCount).to.equal(1);
      expect(handlePointerDown.callCount).to.equal(1);
    });

    it.skipIf(isJSDOM)('should call onValueChange when clicking', async () => {
      const handleChange = spy();

      await render(
        <Tabs.Root value={0} onValueChange={handleChange}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      fireEvent.click(screen.getAllByRole('tab')[1]);
      expect(handleChange.callCount).to.equal(1);
      expect(handleChange.firstCall.args[0]).to.equal(1);
      expect(handleChange.firstCall.args[1].activationDirection).to.equal('right');
    });

    it('should not call onValueChange on non-main button clicks', async () => {
      const handleChange = spy();

      await render(
        <Tabs.Root value={0} onValueChange={handleChange}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      fireEvent.click(screen.getAllByRole('tab')[1], { button: 2 });
      expect(handleChange.callCount).to.equal(0);
    });

    it('should not call onValueChange when already active', async () => {
      const handleChange = spy();

      await render(
        <Tabs.Root value={0} onValueChange={handleChange}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      fireEvent.click(screen.getAllByRole('tab')[0]);
      expect(handleChange.callCount).to.equal(0);
    });

    it('when `activateOnFocus = true` should call onValueChange if an unactive tab gets focused', async () => {
      const handleChange = spy();

      await render(
        <Tabs.Root value={0} onValueChange={handleChange}>
          <Tabs.List activateOnFocus>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      const [firstTab] = screen.getAllByRole('tab');

      await act(async () => {
        firstTab.focus();
      });

      fireEvent.keyDown(firstTab, { key: 'ArrowRight' });
      await flushMicrotasks();

      expect(handleChange.callCount).to.equal(1);
      expect(handleChange.firstCall.args[0]).to.equal(1);
    });

    it('when `activateOnFocus = false` should not call onValueChange if an unactive tab gets focused', async () => {
      const handleChange = spy();

      await render(
        <Tabs.Root value={1} onValueChange={handleChange}>
          <Tabs.List activateOnFocus={false}>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      const [firstTab] = screen.getAllByRole('tab');

      await act(async () => {
        firstTab.focus();
      });

      expect(handleChange.callCount).to.equal(0);
    });
  });

  describe('prop: orientation', () => {
    it('does not add aria-orientation by default', async () => {
      await render(
        <Tabs.Root value={0}>
          <Tabs.List>
            <Tabs.Root />
          </Tabs.List>
        </Tabs.Root>,
      );

      expect(screen.getByRole('tablist')).not.to.have.attribute('aria-orientation');
    });

    it('adds the proper aria-orientation when vertical', async () => {
      await render(
        <Tabs.Root value={0} orientation="vertical">
          <Tabs.List>
            <Tabs.Root />
          </Tabs.List>
        </Tabs.Root>,
      );

      expect(screen.getByRole('tablist')).to.have.attribute('aria-orientation', 'vertical');
    });
  });

  describe('pointer navigation', () => {
    it('selects the clicked tab', async () => {
      const { user } = await render(
        <Tabs.Root defaultValue={0}>
          <Tabs.List activateOnFocus={false}>
            <Tabs.Tab value={0}>Tab 1</Tabs.Tab>
            <Tabs.Tab value={1}>Tab 2</Tabs.Tab>
            <Tabs.Tab value={2}>Tab 3</Tabs.Tab>
          </Tabs.List>
          <Tabs.Panel value={0} keepMounted>
            Panel 1
          </Tabs.Panel>
          <Tabs.Panel value={1} keepMounted>
            Panel 2
          </Tabs.Panel>
          <Tabs.Panel value={2} keepMounted>
            Panel 3
          </Tabs.Panel>
        </Tabs.Root>,
      );

      const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
      await user.click(tab2);

      const panels = screen.getAllByRole('tabpanel', { hidden: true });

      expect(panels[0]).to.have.attribute('hidden');
      expect(panels[1]).not.to.have.attribute('hidden');
      expect(panels[2]).to.have.attribute('hidden');
    });

    it('does not select the clicked disabled tab', async () => {
      const { user } = await render(
        <Tabs.Root defaultValue={0}>
          <Tabs.List activateOnFocus={false}>
            <Tabs.Tab value={0}>Tab 1</Tabs.Tab>
            <Tabs.Tab disabled value={1}>
              Tab 2
            </Tabs.Tab>
            <Tabs.Tab value={2}>Tab 3</Tabs.Tab>
          </Tabs.List>
          <Tabs.Panel value={0} keepMounted>
            Panel 1
          </Tabs.Panel>
          <Tabs.Panel value={1} keepMounted>
            Panel 2
          </Tabs.Panel>
          <Tabs.Panel value={2} keepMounted>
            Panel 3
          </Tabs.Panel>
        </Tabs.Root>,
      );

      const tab2 = screen.getByRole('tab', { name: 'Tab 2' });
      await user.click(tab2);

      const panels = screen.getAllByRole('tabpanel', { hidden: true });

      expect(panels[0]).not.to.have.attribute('hidden');
      expect(panels[1]).to.have.attribute('hidden');
      expect(panels[2]).to.have.attribute('hidden');
    });
  });

  describe('keyboard navigation when focus is on a tab', () => {
    [
      ['horizontal', 'ltr', 'ArrowLeft', 'ArrowRight'],
      ['horizontal', 'rtl', 'ArrowRight', 'ArrowLeft'],
      ['vertical', undefined, 'ArrowUp', 'ArrowDown'],
    ].forEach((entry) => {
      const [orientation, direction, previousItemKey, nextItemKey] = entry;

      describe.skipIf(isJSDOM && direction === 'rtl')(
        `when focus is on a tab element in a ${orientation} ${direction ?? ''} tablist`,
        () => {
          describe(previousItemKey ?? '', () => {
            describe('with `activateOnFocus = false`', () => {
              it('moves focus to the last tab without activating it if focus is on the first tab', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={0}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, , lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  firstTab.focus();
                });

                fireEvent.keyDown(firstTab, { key: previousItemKey });
                await flushMicrotasks();

                expect(lastTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to the previous tab without activating it', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={1}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, secondTab] = screen.getAllByRole('tab');
                await act(async () => {
                  secondTab.focus();
                });

                fireEvent.keyDown(secondTab, { key: previousItemKey });
                await flushMicrotasks();

                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to a disabled tab without activating it', async () => {
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={2}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} disabled />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [, disabledTab, lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  lastTab.focus();
                });

                fireEvent.keyDown(lastTab, { key: previousItemKey });
                await flushMicrotasks();

                expect(disabledTab).toHaveFocus();
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });
            });

            describe('with `activateOnFocus = true`', () => {
              it('moves focus to the last tab while activating it if focus is on the first tab', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={0}
                    >
                      <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, , lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  firstTab.focus();
                });

                fireEvent.keyDown(firstTab, { key: previousItemKey });
                await flushMicrotasks();

                expect(lastTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(1);
                expect(handleChange.firstCall.args[0]).to.equal(2);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to the previous tab while activating it', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={1}
                    >
                      <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, secondTab] = screen.getAllByRole('tab');
                await act(async () => {
                  secondTab.focus();
                });

                fireEvent.keyDown(secondTab, { key: previousItemKey });
                await flushMicrotasks();

                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(1);
                expect(handleChange.firstCall.args[0]).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });
            });

            it('moves focus to a disabled tab without activating it', async () => {
              const handleKeyDown = spy();

              await render(
                <DirectionProvider direction={direction as TextDirection}>
                  <Tabs.Root orientation={orientation as Tabs.Root.Props['orientation']} value={2}>
                    <Tabs.List onKeyDown={handleKeyDown}>
                      <Tabs.Tab value={0} />
                      <Tabs.Tab value={1} disabled />
                      <Tabs.Tab value={2} />
                    </Tabs.List>
                  </Tabs.Root>
                </DirectionProvider>,
              );

              const [, disabledTab, lastTab] = screen.getAllByRole('tab');
              await act(async () => {
                lastTab.focus();
              });

              fireEvent.keyDown(lastTab, { key: previousItemKey });
              await flushMicrotasks();

              expect(disabledTab).toHaveFocus();
              expect(handleKeyDown.callCount).to.equal(1);
              expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
            });
          });

          describe(nextItemKey ?? '', () => {
            describe('with `activateOnFocus = false`', () => {
              it('moves focus to the first tab without activating it if focus is on the last tab', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={2}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, , lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  lastTab.focus();
                });

                fireEvent.keyDown(lastTab, { key: nextItemKey });
                await flushMicrotasks();

                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to the next tab without activating it', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={1}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [, secondTab, lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  secondTab.focus();
                });

                fireEvent.keyDown(secondTab, { key: nextItemKey });
                await flushMicrotasks();

                expect(lastTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to a disabled tab without activating it', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={0}
                    >
                      <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} disabled />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, disabledTab, thirdTab] = screen.getAllByRole('tab');
                await act(async () => {
                  firstTab.focus();
                });

                fireEvent.keyDown(firstTab, { key: nextItemKey });
                await flushMicrotasks();

                expect(disabledTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);

                fireEvent.keyDown(disabledTab, { key: nextItemKey });
                await flushMicrotasks();
                expect(thirdTab).toHaveFocus();
              });
            });

            describe('with `activateOnFocus = true`', () => {
              it('moves focus to the first tab while activating it if focus is on the last tab', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={2}
                    >
                      <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab, , lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  lastTab.focus();
                });

                fireEvent.keyDown(lastTab, { key: nextItemKey });
                await flushMicrotasks();

                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(1);
                expect(handleChange.firstCall.args[0]).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });

              it('moves focus to the next tab while activating it', async () => {
                const handleChange = spy();
                const handleKeyDown = spy();

                await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={1}
                    >
                      <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [, secondTab, lastTab] = screen.getAllByRole('tab');
                await act(async () => {
                  secondTab.focus();
                });

                fireEvent.keyDown(secondTab, { key: nextItemKey });
                await flushMicrotasks();

                expect(lastTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(1);
                expect(handleChange.firstCall.args[0]).to.equal(2);
                expect(handleKeyDown.callCount).to.equal(1);
                expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
              });
            });

            it('moves focus to a disabled tab without activating it', async () => {
              const handleChange = spy();
              const handleKeyDown = spy();

              await render(
                <DirectionProvider direction={direction as TextDirection}>
                  <Tabs.Root
                    onValueChange={handleChange}
                    orientation={orientation as Tabs.Root.Props['orientation']}
                    value={0}
                  >
                    <Tabs.List onKeyDown={handleKeyDown}>
                      <Tabs.Tab value={0} />
                      <Tabs.Tab value={1} disabled />
                      <Tabs.Tab value={2} />
                    </Tabs.List>
                  </Tabs.Root>
                </DirectionProvider>,
              );

              const [firstTab, disabledTab, thirdTab] = screen.getAllByRole('tab');
              await act(async () => {
                firstTab.focus();
              });

              fireEvent.keyDown(firstTab, { key: nextItemKey });
              await flushMicrotasks();

              expect(disabledTab).toHaveFocus();
              expect(handleChange.callCount).to.equal(0);
              expect(handleKeyDown.callCount).to.equal(1);
              expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);

              fireEvent.keyDown(disabledTab, { key: nextItemKey });
              await flushMicrotasks();
              expect(thirdTab).toHaveFocus();
            });
          });

          describe('modifier keys', () => {
            ['Shift', 'Control', 'Alt', 'Meta'].forEach((modifierKey) => {
              it(`does not move focus when modifier key: ${modifierKey} is pressed`, async () => {
                const handleChange = spy();
                const handleKeyDown = spy();
                const { user } = await render(
                  <DirectionProvider direction={direction as TextDirection}>
                    <Tabs.Root
                      onValueChange={handleChange}
                      orientation={orientation as Tabs.Root.Props['orientation']}
                      value={0}
                    >
                      <Tabs.List onKeyDown={handleKeyDown}>
                        <Tabs.Tab value={0} />
                        <Tabs.Tab value={1} />
                        <Tabs.Tab value={2} />
                      </Tabs.List>
                    </Tabs.Root>
                  </DirectionProvider>,
                );

                const [firstTab] = screen.getAllByRole('tab');

                await user.keyboard('[Tab]');
                expect(firstTab).toHaveFocus();

                await user.keyboard(`{${modifierKey}>}{${nextItemKey}}`);
                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(2);

                await user.keyboard(`{${modifierKey}>}{${previousItemKey}}`);
                expect(firstTab).toHaveFocus();
                expect(handleChange.callCount).to.equal(0);
                expect(handleKeyDown.callCount).to.equal(4);
              });
            });
          });
        },
      );
    });

    describe('when focus is on a tab regardless of orientation', () => {
      describe('Home', () => {
        it('when `activateOnFocus = false`, moves focus to the first tab without activating it', async () => {
          const handleChange = spy();
          const handleKeyDown = spy();

          await render(
            <Tabs.Root onValueChange={handleChange} value={2}>
              <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                <Tabs.Tab value={0} />
                <Tabs.Tab value={1} />
                <Tabs.Tab value={2} />
              </Tabs.List>
            </Tabs.Root>,
          );

          const [firstTab, , lastTab] = screen.getAllByRole('tab');
          await act(async () => {
            lastTab.focus();
          });

          fireEvent.keyDown(lastTab, { key: 'Home' });
          await flushMicrotasks();

          expect(firstTab).toHaveFocus();
          expect(handleChange.callCount).to.equal(0);
          expect(handleKeyDown.callCount).to.equal(1);
          expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
        });

        it('when `activateOnFocus = true`, moves focus to the first tab while activating it', async () => {
          const handleChange = spy();
          const handleKeyDown = spy();

          await render(
            <Tabs.Root onValueChange={handleChange} value={2}>
              <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                <Tabs.Tab value={0} />
                <Tabs.Tab value={1} />
                <Tabs.Tab value={2} />
              </Tabs.List>
            </Tabs.Root>,
          );

          const [firstTab, , lastTab] = screen.getAllByRole('tab');
          await act(async () => {
            lastTab.focus();
          });

          fireEvent.keyDown(lastTab, { key: 'Home' });
          await flushMicrotasks();

          expect(firstTab).toHaveFocus();
          expect(handleChange.callCount).to.equal(1);
          expect(handleChange.firstCall.args[0]).to.equal(0);
          expect(handleKeyDown.callCount).to.equal(1);
          expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
        });

        [false, true].forEach((activateOnFocusProp) => {
          it(`when \`activateOnFocus = ${activateOnFocusProp}\`, moves focus to a disabled tab without activating it`, async () => {
            const handleChange = spy();
            const handleKeyDown = spy();

            await render(
              <Tabs.Root onValueChange={handleChange} value={2}>
                <Tabs.List activateOnFocus={activateOnFocusProp} onKeyDown={handleKeyDown}>
                  <Tabs.Tab value={0} disabled />
                  <Tabs.Tab value={1} />
                  <Tabs.Tab value={2} />
                </Tabs.List>
              </Tabs.Root>,
            );

            const [disabledTab, , lastTab] = screen.getAllByRole('tab');
            await act(async () => {
              lastTab.focus();
            });

            fireEvent.keyDown(lastTab, { key: 'Home' });
            await flushMicrotasks();

            expect(disabledTab).toHaveFocus();
            expect(handleChange.callCount).to.equal(0);
            expect(handleKeyDown.callCount).to.equal(1);
            expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
          });
        });
      });

      describe('End', () => {
        it('when `activateOnFocus = false`, moves focus to the last tab without activating it', async () => {
          const handleChange = spy();
          const handleKeyDown = spy();

          await render(
            <Tabs.Root onValueChange={handleChange} value={0}>
              <Tabs.List activateOnFocus={false} onKeyDown={handleKeyDown}>
                <Tabs.Tab value={0} />
                <Tabs.Tab value={1} />
                <Tabs.Tab value={2} />
              </Tabs.List>
            </Tabs.Root>,
          );

          const [firstTab, , lastTab] = screen.getAllByRole('tab');
          await act(async () => {
            firstTab.focus();
          });

          fireEvent.keyDown(firstTab, { key: 'End' });
          await flushMicrotasks();

          expect(lastTab).toHaveFocus();
          expect(handleChange.callCount).to.equal(0);
          expect(handleKeyDown.callCount).to.equal(1);
          expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
        });

        it('when `activateOnFocus = true`, moves focus to the last tab while activating it', async () => {
          const handleChange = spy();
          const handleKeyDown = spy();

          await render(
            <Tabs.Root onValueChange={handleChange} value={0}>
              <Tabs.List onKeyDown={handleKeyDown} activateOnFocus>
                <Tabs.Tab value={0} />
                <Tabs.Tab value={1} />
                <Tabs.Tab value={2} />
              </Tabs.List>
            </Tabs.Root>,
          );

          const [firstTab, , lastTab] = screen.getAllByRole('tab');
          await act(async () => {
            firstTab.focus();
          });

          fireEvent.keyDown(firstTab, { key: 'End' });
          await flushMicrotasks();

          expect(lastTab).toHaveFocus();
          expect(handleChange.callCount).to.equal(1);
          expect(handleChange.firstCall.args[0]).to.equal(2);
          expect(handleKeyDown.callCount).to.equal(1);
          expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
        });

        [false, true].forEach((activateOnFocusProp) => {
          it(`when \`activateOnFocus = ${activateOnFocusProp}\`, moves focus to a disabled tab without activating it`, async () => {
            const handleChange = spy();
            const handleKeyDown = spy();

            await render(
              <Tabs.Root onValueChange={handleChange} value={0}>
                <Tabs.List activateOnFocus={activateOnFocusProp} onKeyDown={handleKeyDown}>
                  <Tabs.Tab value={0} />
                  <Tabs.Tab value={1} />
                  <Tabs.Tab value={2} disabled />
                </Tabs.List>
              </Tabs.Root>,
            );

            const [firstTab, , disabledTab] = screen.getAllByRole('tab');
            await act(async () => {
              firstTab.focus();
            });

            fireEvent.keyDown(firstTab, { key: 'End' });
            await flushMicrotasks();

            expect(disabledTab).toHaveFocus();
            expect(handleChange.callCount).to.equal(0);
            expect(handleKeyDown.callCount).to.equal(1);
            expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true);
          });
        });
      });
    });

    it('should allow to focus first tab when there are no active tabs', async () => {
      await render(
        <Tabs.Root defaultValue={0}>
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      expect(screen.getAllByRole('tab').map((tab) => tab.getAttribute('tabIndex'))).to.deep.equal([
        '0',
        '-1',
      ]);
    });
  });

  describe.skipIf(isJSDOM)('activation direction', () => {
    it('should set the `data-activation-direction` attribute on the tabs root with orientation=horizontal', async () => {
      await render(
        <Tabs.Root data-testid="root">
          <Tabs.List>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
          </Tabs.List>
        </Tabs.Root>,
      );

      const root = screen.getByTestId('root');
      const [tab1, tab2] = screen.getAllByRole('tab');

      expect(root).to.have.attribute('data-activation-direction', 'none');
      await act(async () => {
        tab2.click();
      });

      expect(root).to.have.attribute('data-activation-direction', 'right');

      await act(async () => {
        tab1.click();
      });

      expect(root).to.have.attribute('data-activation-direction', 'left');
    });

    it('should set the `data-activation-direction` attribute on the tabs root with orientation=vertical', async () => {
      await render(
        <Tabs.Root data-testid="root" orientation="vertical">
          <Tabs.List>
            <Tabs.Tab value={0} style={{ display: 'block' }} />
            <Tabs.Tab value={1} style={{ display: 'block' }} />
          </Tabs.List>
        </Tabs.Root>,
      );

      const root = screen.getByTestId('root');
      const [tab1, tab2] = screen.getAllByRole('tab');

      expect(root).to.have.attribute('data-activation-direction', 'none');
      await act(async () => {
        tab2.click();
      });

      expect(root).to.have.attribute('data-activation-direction', 'down');

      await act(async () => {
        tab1.click();
      });

      expect(root).to.have.attribute('data-activation-direction', 'up');
    });
  });

  describe('popups', () => {
    it('works inside Popover', async () => {
      function ExamplePopover() {
        return (
          <Popover.Root>
            <Popover.Trigger>Open</Popover.Trigger>
            <Popover.Portal>
              <Popover.Positioner sideOffset={8}>
                <Popover.Popup>
                  <Tabs.Root defaultValue="overview">
                    <Tabs.List>
                      <Tabs.Tab value="overview">Overview</Tabs.Tab>
                      <Tabs.Tab value="projects">Projects</Tabs.Tab>
                      <Tabs.Tab value="account">Account</Tabs.Tab>
                    </Tabs.List>
                    <Tabs.Panel value="overview" />
                    <Tabs.Panel value="projects" />
                    <Tabs.Panel value="account" />
                  </Tabs.Root>
                </Popover.Popup>
              </Popover.Positioner>
            </Popover.Portal>
          </Popover.Root>
        );
      }

      const { user } = await render(<ExamplePopover />);

      const trigger = screen.getByRole('button', { name: 'Open' });

      await user.click(trigger);

      const tab1 = screen.getByRole('tab', { name: 'Overview' });
      await waitFor(() => {
        expect(tab1).toHaveFocus();
      });

      await user.keyboard('{ArrowRight}');

      const tab2 = screen.getByRole('tab', { name: 'Projects' });
      await waitFor(() => {
        expect(tab2).toHaveFocus();
      });
    });

    it('works inside Dialog', async () => {
      function ExampleDialog() {
        return (
          <Dialog.Root>
            <Dialog.Trigger>Open</Dialog.Trigger>
            <Dialog.Portal>
              <Dialog.Popup>
                <Tabs.Root defaultValue="overview">
                  <Tabs.List>
                    <Tabs.Tab value="overview">Overview</Tabs.Tab>
                    <Tabs.Tab value="projects">Projects</Tabs.Tab>
                    <Tabs.Tab value="account">Account</Tabs.Tab>
                  </Tabs.List>
                  <Tabs.Panel value="overview" />
                  <Tabs.Panel value="projects" />
                  <Tabs.Panel value="account" />
                </Tabs.Root>
              </Dialog.Popup>
            </Dialog.Portal>
          </Dialog.Root>
        );
      }

      const { user } = await render(<ExampleDialog />);

      const trigger = screen.getByRole('button', { name: 'Open' });

      await user.click(trigger);

      const tab1 = screen.getByRole('tab', { name: 'Overview' });
      await waitFor(() => {
        expect(tab1).toHaveFocus();
      });
      await user.keyboard('{ArrowRight}');

      const tab2 = screen.getByRole('tab', { name: 'Projects' });
      await waitFor(() => {
        expect(tab2).toHaveFocus();
      });
    });
  });

  describe('highlight synchronization on external value change relative to focus', () => {
    it('when focus is outside the tablist, highlight follows the new active tab (tabIndex=0 moves)', async () => {
      const { setProps } = await render(
        <Tabs.Root value={0}>
          <Tabs.List activateOnFocus={false}>
            <Tabs.Tab value={0} />
            <Tabs.Tab value={1} />
            <Tabs.Tab value={2} />
          </Tabs.List>
        </Tabs.Root>,
      );

      const [firstTab, secondTab, thirdTab] = screen.getAllByRole('tab');

      expect(firstTab.tabIndex).to.equal(0);

      await setProps({ value: 2 });
      await flushMicrotasks();

      expect(firstTab.tabIndex).to.equal(-1);
      expect(secondTab.tabIndex).to.equal(-1);
      expect(thirdTab.tabIndex).to.equal(0);

      await setProps({ value: 1 });
      await flushMicrotasks();

      expect(firstTab.tabIndex).to.equal(-1);
      expect(secondTab.tabIndex).to.equal(0);
      expect(thirdTab.tabIndex).to.equal(-1);
    });

    it('when focus is inside the tablist, highlight stays put on external change and arrow keys continue from the focused tab', async () => {
      const { setProps } = await render(
        <Tabs.Root value={0}>
          <Tabs.List activateOnFocus={false}>
            <Tabs.Tab value={0}>Tab 1</Tabs.Tab>
            <Tabs.Tab value={1}>Tab 2</Tabs.Tab>
            <Tabs.Tab value={2}>Tab 3</Tabs.Tab>
          </Tabs.List>
        </Tabs.Root>,
      );

      const [firstTab, secondTab, thirdTab] = screen.getAllByRole('tab');

      await act(async () => {
        firstTab.focus();
      });
      expect(firstTab).to.have.property('tabIndex', 0);

      await setProps({ value: 2 });
      await flushMicrotasks();

      // Highlight should not change (still on first tab), but selection did
      expect(firstTab.tabIndex).to.equal(0);
      expect(secondTab.tabIndex).to.equal(-1);
      expect(thirdTab.tabIndex).to.equal(-1);
      expect(firstTab).to.have.attribute('aria-selected', 'false');
      expect(thirdTab).to.have.attribute('aria-selected', 'true');

      // Arrow navigation should continue from the highlighted tab
      fireEvent.keyDown(firstTab, { key: 'ArrowRight' });
      await flushMicrotasks();

      expect(secondTab).toHaveFocus();
      // Selection remains the externally-set tab since activateOnFocus=false
      expect(thirdTab).to.have.attribute('aria-selected', 'true');
      expect(secondTab).to.have.attribute('aria-selected', 'false');
    });
  });
});
